ποΈGitΠ―ΡΠ°ποΈ
Commit 07975052daf37675358098b38e1d558f3df42c8e
Parents : 7617f1d
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-06-28T19:53:48-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-06-28T19:53:48-05:00
chore(ci): prune dead workflows, move vuln scanning to Renovate, fix changelog gaps (#6000)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Changes
8 files changed, 8 insertions(+), 861 deletions(-)
Diff
diff --git a/.github/release.yml b/.github/release.yml
index c029baaf95..0ad30dc312 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -2,7 +2,6 @@
#
# Labels here must match actual repo labels. Run `gh label list` to verify.
# Auto-labeler: .github/workflows/pull-request-target.yml
-# AI classifier: .github/workflows/models_pr_triage.yml
changelog:
exclude:
@@ -16,7 +15,6 @@ changelog:
- ci
- build
- testing
- - refactor
- documentation
- l10n
authors:
diff --git a/.github/renovate.json b/.github/renovate.json
index ba277db377..d2dfea94e6 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -9,6 +9,7 @@
"workarounds:all"
],
"commitMessageTopic": "{{depName}}",
+ "osvVulnerabilityAlerts": true,
"labels": [
"dependencies"
],
diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml
deleted file mode 100644
index 5a8d203c37..0000000000
--- a/.github/workflows/dependency-submission.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Dependency Submission
-
-on:
- push:
- branches: [ 'main' ]
- workflow_dispatch:
-
-permissions:
- contents: write
-
-jobs:
- dependency-submission:
- runs-on: ubuntu-24.04
- if: github.repository == 'meshtastic/Meshtastic-Android'
-
- steps:
- - uses: actions/checkout@v7.0.0
- - uses: actions/setup-java@v5
- with:
- distribution: temurin
- java-version: 21
- token: ${{ github.token }}
-
- - name: Generate and submit dependency graph
- uses: gradle/actions/dependency-submission@v6
- with:
- build-scan-publish: true
- build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
- build-scan-terms-of-use-agree: "yes"
diff --git a/.github/workflows/docs-governance.yml b/.github/workflows/docs-governance.yml
deleted file mode 100644
index 4f014b3de1..0000000000
--- a/.github/workflows/docs-governance.yml
+++ /dev/null
@@ -1,419 +0,0 @@
-name: UI & Docs Governance
-
-# Split by trust level:
-# - pull_request (fork context, read-only token, NO secrets) runs the `validate`
-# job, which checks out and EXECUTES fork-supplied code (node scripts/*.js). This
-# is safe only because the token is read-only and secrets are unavailable.
-# - pull_request_target (base context, write token) runs the comment-posting
-# staleness jobs. They MUST NOT check out or execute fork code β they read the
-# changed-file list via the API instead. (actions/checkout@v7 also refuses a fork
-# checkout under pull_request_target, which is what surfaced this.)
-on:
- pull_request:
- branches: [main]
- types: [opened, synchronize, reopened]
- pull_request_target:
- branches: [main]
- types: [opened, synchronize, reopened, labeled, unlabeled]
- schedule:
- - cron: "0 6 * * 1" # Every Monday at 6 AM UTC
- workflow_dispatch:
-
-permissions:
- contents: read
- pull-requests: write
-
-jobs:
- # ββ Job 1: Staleness β flag PRs that change UI without updating docs ββββββββ
- staleness:
- name: Docs staleness check
- runs-on: ubuntu-24.04-arm
- # pull_request_target only: needs the write token to post advisory comments.
- # Reads the changed-file list via the API β no fork checkout, no fork code run.
- if: >-
- github.event_name == 'pull_request_target'
- && github.event.pull_request != null
- && !contains(github.event.pull_request.labels.*.name, 'skip-docs-check')
-
- steps:
- - name: Detect changed files
- id: changed
- env:
- GH_TOKEN: ${{ github.token }}
- run: |
- changed=$(gh pr diff "${{ github.event.pull_request.number }}" \
- --repo "${{ github.repository }}" --name-only)
-
- views_changed=$(echo "$changed" | grep -E \
- '^feature/.*/src/commonMain/.*/(ui|component|screen)/|^feature/.*/src/androidMain/.*/ui/|^core/ui/src/commonMain/' \
- | grep -v 'Test\|Preview\|__Snapshots__' || true)
-
- docs_changed=$(echo "$changed" | grep -E '^docs/en/(user|developer)/' || true)
-
- echo "views_changed<<EOF" >> "$GITHUB_OUTPUT"
- echo "$views_changed" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
-
- echo "docs_changed<<EOF" >> "$GITHUB_OUTPUT"
- echo "$docs_changed" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
-
- if [[ -n "$views_changed" && -z "$docs_changed" ]]; then
- echo "stale=true" >> "$GITHUB_OUTPUT"
- else
- echo "stale=false" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Post warning comment
- if: steps.changed.outputs.stale == 'true'
- uses: actions/github-script@v9
- with:
- script: |
- const viewsChanged = `${{ steps.changed.outputs.views_changed }}`.trim();
- const body = [
- '## π Docs staleness check β advisory',
- '',
- 'This PR modifies user-facing UI source files but does not update any page under `docs/en/user/` or `docs/en/developer/`.',
- '',
- '> β οΈ Doc changes propagate to **3 consumers**: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in `docs/en/` automatically flows to all three.',
- '',
- '**Changed source files:**',
- '```',
- viewsChanged,
- '```',
- '',
- '**What to check:**',
- '| Changed area | Likely doc page |',
- '|---|---|',
- '| `feature/messaging/` | `docs/en/user/messages-and-channels.md` |',
- '| `feature/node/` | `docs/en/user/nodes.md` or `docs/en/user/node-metrics.md` |',
- '| `feature/map/` | `docs/en/user/map-and-waypoints.md` |',
- '| `feature/connections/` | `docs/en/user/connections.md` |',
- '| `feature/settings/` | `docs/en/user/settings-radio-user.md` or `docs/en/user/settings-module-admin.md` |',
- '| `feature/firmware/` | `docs/en/user/firmware.md` |',
- '| `feature/intro/` | `docs/en/user/onboarding.md` |',
- '| `feature/discovery/` | `docs/en/user/discovery.md` |',
- '| `feature/docs/` | Internal docs infrastructure |',
- '| `core/ui/` | `docs/en/developer/codebase.md` or component-specific user pages |',
- '',
- '**New page checklist** (if adding a new doc page):',
- '1. Create the `.md` file in `docs/en/user/` or `docs/en/developer/` with `last_updated` frontmatter',
- '2. Register in `DocBundleLoader.kt` with string resources (in-app browser)',
- '3. Jekyll and Docusaurus sync pick up new pages automatically β no config change needed',
- '',
- 'If this PR does **not** require a doc update (e.g., internal refactor, bug fix, test change), add the **`skip-docs-check`** label to dismiss this check.',
- '',
- '> **Cross-platform note:** This check is advisory while doc coverage matures. Both Android and Apple repos use the same `skip-docs-check` label and advisory severity. See `meshtastic/design` standards for shared conventions.',
- ].join('\n');
-
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
- const existing = comments.find(c =>
- c.user.login === 'github-actions[bot]' &&
- c.body.includes('Docs staleness check')
- );
- if (existing) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing.id,
- body,
- });
- } else {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body,
- });
- }
-
- - name: Dismiss stale comment when docs are updated
- if: steps.changed.outputs.stale == 'false'
- uses: actions/github-script@v9
- with:
- script: |
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
- const existing = comments.find(c =>
- c.user.login === 'github-actions[bot]' &&
- c.body.includes('Docs staleness check')
- );
- if (existing) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing.id,
- body: '## β
Docs staleness check passed\n\nThis PR includes updates to `docs/en/` alongside the source changes. Thank you!',
- });
- }
-
- - name: Advisory status
- if: steps.changed.outputs.stale == 'true'
- run: |
- echo "::warning::UI source files changed without corresponding docs/en/ updates."
- echo "Add the 'skip-docs-check' label if this PR does not require a doc update."
- echo "NOTE: This check is advisory while docs coverage matures across platforms."
- echo "To upgrade to blocking, change this step to 'exit 1'."
-
- # ββ Job 2: Quality gates β link validation, coverage, registry, freshness βββ
- validate:
- name: Docs quality gates
- runs-on: ubuntu-24.04-arm
- # Runs on pull_request (fork context, read-only token, no secrets) β never on
- # pull_request_target β because it checks out and executes fork-supplied code
- # (node scripts/*.js). The pull_request event makes that execution safe; the
- # pull_request_target instance is skipped to avoid the pwn-request RCE and the
- # actions/checkout@v7 fork-checkout refusal.
- if: github.event_name != 'pull_request_target'
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7.0.0
- with:
- ref: ${{ github.event.pull_request.head.sha || github.sha }}
- fetch-depth: 1
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version: "24"
-
- - name: Validate internal links
- run: node scripts/validate-doc-links.js docs/en
-
- - name: Check doc coverage
- run: node scripts/check-doc-coverage.js .
-
- - name: Validate DocBundleLoader registry
- run: |
- loader="feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt"
- missing=0
- for f in docs/en/user/*.md docs/en/developer/*.md; do
- slug=$(basename "$f" .md)
- if ! grep -q "\"$slug\"" "$loader"; then
- echo "ERROR: $slug not registered in DocBundleLoader.kt"
- missing=$((missing + 1))
- fi
- done
- if [ "$missing" -gt 0 ]; then
- echo ""
- echo "FAILED: $missing page(s) missing from DocBundleLoader.kt in-app index."
- exit 1
- fi
- echo "All doc pages registered in DocBundleLoader."
-
- - name: Check doc freshness
- # Advisory β warns on stale pages but does not block PRs
- continue-on-error: true
- run: node scripts/check-doc-freshness.js docs --max-age-days=180
-
- # ββ Job 3: Preview staleness β flag UI changes without preview updates ββββββ
- preview-staleness:
- name: Preview staleness check
- runs-on: ubuntu-24.04-arm
- # pull_request_target only: needs the write token to post advisory comments.
- # Reads the changed-file list via the API β no fork checkout, no fork code run.
- if: >-
- github.event_name == 'pull_request_target'
- && github.event.pull_request != null
- && !contains(github.event.pull_request.labels.*.name, 'skip-preview-check')
-
- steps:
- - name: Detect changed files
- id: changed
- env:
- GH_TOKEN: ${{ github.token }}
- run: |
- changed=$(gh pr diff "${{ github.event.pull_request.number }}" \
- --repo "${{ github.repository }}" --name-only)
-
- # UI composables changed (screens, components β excluding tests and previews)
- ui_changed=$(echo "$changed" | grep -E \
- '^(feature|core/ui)/.*/src/commonMain/.*/.*\.(kt)$' \
- | grep -E '/(ui|component|screen)/' \
- | grep -v 'Test\|Preview\|__Snapshots__' || true)
-
- # Preview files changed
- preview_changed=$(echo "$changed" | grep -E 'Preview.*\.kt$' || true)
-
- # Screenshot test files changed (regression gate + generate-only docs module)
- screenshot_tests_changed=$(echo "$changed" | grep -E '^(screenshot-tests|docs-screenshots)/src/screenshotTest/.*\.kt$' || true)
-
- # Reference images changed (either module)
- refs_changed=$(echo "$changed" | grep -E '^(screenshot-tests|docs-screenshots)/src/screenshotTestDebug/reference/.*\.png$' || true)
-
- echo "ui_changed<<EOF" >> "$GITHUB_OUTPUT"
- echo "$ui_changed" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
-
- echo "preview_changed<<EOF" >> "$GITHUB_OUTPUT"
- echo "$preview_changed" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
-
- echo "screenshot_tests_changed<<EOF" >> "$GITHUB_OUTPUT"
- echo "$screenshot_tests_changed" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
-
- echo "refs_changed<<EOF" >> "$GITHUB_OUTPUT"
- echo "$refs_changed" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
-
- # Preview staleness: UI changed but no preview updates
- if [[ -n "$ui_changed" && -z "$preview_changed" ]]; then
- echo "preview_stale=true" >> "$GITHUB_OUTPUT"
- else
- echo "preview_stale=false" >> "$GITHUB_OUTPUT"
- fi
-
- # Screenshot staleness: previews changed but no reference image updates
- if [[ -n "$preview_changed" && -z "$refs_changed" ]]; then
- echo "screenshot_stale=true" >> "$GITHUB_OUTPUT"
- else
- echo "screenshot_stale=false" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Post preview advisory
- if: steps.changed.outputs.preview_stale == 'true'
- uses: actions/github-script@v9
- with:
- script: |
- const uiChanged = `${{ steps.changed.outputs.ui_changed }}`.trim();
- const body = [
- '## πΌοΈ Preview staleness check β advisory',
- '',
- 'This PR modifies UI composables but does not update any `*Previews.kt` files.',
- '',
- '> Previews power screenshot tests and in-app docs screenshots. Keeping them current ensures visual regression coverage stays accurate.',
- '',
- '**Changed UI files:**',
- '```',
- uiChanged,
- '```',
- '',
- '**What to check:**',
- '| Pattern | Preview file convention |',
- '|---|---|',
- '| `feature/{name}/β¦/ui/` or `component/` | `feature/{name}/β¦/*Previews.kt` |',
- '| `core/ui/β¦/` | `core/ui/β¦/` (previews colocated) |',
- '',
- '**Adding previews checklist:**',
- '1. Create or update a `*Previews.kt` file in the same module with `@PreviewLightDark`',
- '2. Baseline the public preview in the module `detekt-baseline.xml` (or `@Suppress("PreviewPublic")`) if it is consumed cross-module by a screenshot wrapper',
- '3. Add a `@PreviewTest` wrapper: in `screenshot-tests/src/screenshotTest/` for a regression-gated component, or `docs-screenshots/src/screenshotTest/` for a doc-framed composition',
- '4. Run `./gradlew :screenshot-tests:updateDebugScreenshotTest` (and `:docs-screenshots:updateDebugScreenshotTest` for doc compositions) to generate reference images',
- '',
- 'If this PR does **not** require preview updates (e.g., logic-only change, non-visual refactor), add the **`skip-preview-check`** label to dismiss.',
- ].join('\n');
-
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
- const existing = comments.find(c =>
- c.user.login === 'github-actions[bot]' &&
- c.body.includes('Preview staleness check')
- );
- if (existing) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing.id,
- body,
- });
- } else {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body,
- });
- }
-
- - name: Post screenshot advisory
- if: steps.changed.outputs.screenshot_stale == 'true'
- uses: actions/github-script@v9
- with:
- script: |
- const previewChanged = `${{ steps.changed.outputs.preview_changed }}`.trim();
- const body = [
- '## πΈ Screenshot reference staleness β advisory',
- '',
- 'This PR modifies preview composables but does not update screenshot reference images.',
- '',
- '> Reference images in `screenshot-tests/src/screenshotTestDebug/reference/` must be regenerated when previews change, or `validateDebugScreenshotTest` will fail.',
- '',
- '**Changed preview files:**',
- '```',
- previewChanged,
- '```',
- '',
- '**How to update:**',
- '```bash',
- './gradlew :screenshot-tests:updateDebugScreenshotTest # regression goldens',
- './gradlew :docs-screenshots:updateDebugScreenshotTest # doc-framed compositions',
- './gradlew :screenshot-tests:copyDocsScreenshots # refresh docs/assets from both',
- '```',
- 'Then commit the updated reference PNGs (and any refreshed `docs/assets/screenshots/*.png`).',
- '',
- 'If this change is intentionally preview-only (e.g., adding a preview that doesn\'t need a test yet), add the **`skip-preview-check`** label.',
- ].join('\n');
-
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
- const existing = comments.find(c =>
- c.user.login === 'github-actions[bot]' &&
- c.body.includes('Screenshot reference staleness')
- );
- if (existing) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing.id,
- body,
- });
- } else {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body,
- });
- }
-
- - name: Dismiss comments when resolved
- if: >-
- steps.changed.outputs.preview_stale == 'false'
- && steps.changed.outputs.screenshot_stale == 'false'
- uses: actions/github-script@v9
- with:
- script: |
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
- for (const marker of ['Preview staleness check', 'Screenshot reference staleness']) {
- const existing = comments.find(c =>
- c.user.login === 'github-actions[bot]' &&
- c.body.includes(marker)
- );
- if (existing) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing.id,
- body: `## β
${marker} passed\n\nPreview and screenshot references are up to date.`,
- });
- }
- }
diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml
deleted file mode 100644
index a16b5824d5..0000000000
--- a/.github/workflows/models_issue_triage.yml
+++ /dev/null
@@ -1,203 +0,0 @@
-name: Issue Triage (Models)
-
-on:
- workflow_dispatch:
-
-permissions:
- issues: write
- models: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.issue.number }}
- cancel-in-progress: true
-
-jobs:
- triage:
- if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.issue.user.type != 'Bot' }}
- runs-on: ubuntu-24.04-arm
- steps:
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- - name: Detect spam or low-quality content
- uses: actions/ai-inference@v2
- id: quality
- continue-on-error: true
- with:
- max-tokens: 20
- prompt: |
- Is this GitHub issue spam, AI-generated slop, or low quality?
-
- Title: ${{ github.event.issue.title }}
- Body: ${{ github.event.issue.body }}
-
- Respond with exactly one of: spam, ai-generated, needs-review, ok
- system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
- model: openai/gpt-4o-mini
-
- - name: Apply quality label if needed
- if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
- uses: actions/github-script@v9
- env:
- QUALITY_LABEL: ${{ steps.quality.outputs.response }}
- with:
- script: |
- const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
- const labelMeta = {
- 'spam': { color: 'd73a4a', description: 'Possible spam' },
- 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
- 'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
- };
- const meta = labelMeta[label];
- if (!meta) return;
-
- // Ensure label exists
- try {
- await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
- } catch (e) {
- if (e.status !== 404) throw e;
- await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
- }
-
- // Apply label
- await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
-
- // Set output to skip remaining steps
- core.setOutput('is_spam', 'true');
-
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Step 2: Duplicate detection - only if not spam
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- - name: Detect duplicate issues
- if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
- uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
-
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Step 3: Completeness check + auto-labeling (combined into one AI call)
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- - name: Determine if completeness check should be skipped
- if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
- uses: actions/github-script@v9
- id: check-skip
- with:
- script: |
- const title = (context.payload.issue.title || '').toLowerCase();
- const labels = (context.payload.issue.labels || []).map(label => label.name);
- const hasFeatureRequest = title.includes('feature request');
- const hasEnhancement = labels.includes('enhancement');
- const shouldSkip = hasFeatureRequest && hasEnhancement;
- core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
-
- - name: Analyze issue completeness and determine labels
- if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
- uses: actions/ai-inference@v2
- id: analysis
- continue-on-error: true
- with:
- prompt: |
- Analyze this GitHub issue for the Meshtastic Android app and determine if it needs labels.
-
- If this looks like a bug in the Android app (crash, ANR, UI glitch, connection failure, Bluetooth issues, notification problems, map issues), request app logs and explain how to get them:
-
- Android app debug logs:
- - Open the Meshtastic app, go to Settings > Debug > Save Logs
- - Reproduce the problem, then share/attach the exported log file
-
- Android logcat (if app logs are insufficient):
- - Connect phone via USB with USB debugging enabled
- - Run: adb logcat -s Meshtastic:* *:E
- - Reproduce the problem, then copy/paste the relevant output
-
- Also request key context if missing: Android version, phone model, app version, Meshtastic device model, firmware version, connection type (BLE/USB/TCP), steps to reproduce, expected vs actual.
-
- Respond ONLY with JSON:
- {
- "complete": true|false,
- "comment": "Your helpful comment requesting missing info, or empty string if complete",
- "label": "needs-logs" | "needs-info" | "none"
- }
-
- Use "needs-logs" if this is an app bug AND no logs are attached.
- Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
- Use "none" if the issue is complete or is a feature request.
-
- Title: ${{ github.event.issue.title }}
- Body: ${{ github.event.issue.body }}
- system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels.
- model: openai/gpt-4o-mini
-
- - name: Process analysis result
- if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
- uses: actions/github-script@v9
- id: process
- env:
- AI_RESPONSE: ${{ steps.analysis.outputs.response }}
- with:
- script: |
- const raw = (process.env.AI_RESPONSE || '').trim();
-
- let complete = false;
- let comment = '';
- let label = 'none';
-
- try {
- const parsed = JSON.parse(raw);
- complete = !!parsed.complete;
- comment = (parsed.comment ?? '').toString().trim();
- label = (parsed.label ?? 'none').toString().trim().toLowerCase();
- } catch {
- // If JSON parse fails, treat as incomplete with raw response as comment
- complete = false;
- comment = raw;
- label = 'none';
- }
-
- // Validate label
- const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
- if (!allowedLabels.has(label)) label = 'none';
-
- core.setOutput('should_comment', (!complete && comment.length > 0) ? 'true' : 'false');
- core.setOutput('comment_body', comment);
- core.setOutput('label', label);
-
- - name: Apply triage label
- if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
- uses: actions/github-script@v9
- env:
- LABEL_NAME: ${{ steps.process.outputs.label }}
- with:
- script: |
- const label = process.env.LABEL_NAME;
- const labelMeta = {
- 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
- 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
- };
- const meta = labelMeta[label];
- if (!meta) return;
-
- // Ensure label exists
- try {
- await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
- } catch (e) {
- if (e.status !== 404) throw e;
- await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
- }
-
- // Apply label
- await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
-
- - name: Comment on issue
- if: steps.process.outputs.should_comment == 'true'
- uses: actions/github-script@v9
- env:
- COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
- with:
- script: |
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.payload.issue.number,
- body: process.env.COMMENT_BODY
- });
diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml
deleted file mode 100644
index 386327748f..0000000000
--- a/.github/workflows/models_pr_triage.yml
+++ /dev/null
@@ -1,159 +0,0 @@
-name: PR Triage (Models)
-
-on:
- workflow_dispatch:
-
-permissions:
- pull-requests: write
- issues: write
- models: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
- cancel-in-progress: true
-
-jobs:
- triage:
- if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.pull_request.user.type != 'Bot' }}
- runs-on: ubuntu-24.04-arm
- steps:
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Step 1: Check if PR already has automation/type labels (skip if so)
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- - name: Check existing labels
- uses: actions/github-script@v9
- id: check-labels
- with:
- script: |
- const skipLabels = new Set(['automation', 'release']);
- const typeLabels = new Set(['bugfix', 'enhancement', 'dependencies', 'repo', 'refactor', 'chore', 'ci', 'build', 'testing', 'documentation']);
- const prLabels = context.payload.pull_request.labels.map(l => l.name);
-
- const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
- const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
-
- core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
- core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
-
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Step 2: Quality check (spam/AI-slop detection)
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- - name: Detect spam or low-quality content
- if: steps.check-labels.outputs.skip_all != 'true'
- uses: actions/ai-inference@v2
- id: quality
- continue-on-error: true
- env:
- PR_TITLE: ${{ github.event.pull_request.title }}
- PR_BODY: ${{ github.event.pull_request.body }}
- with:
- max-tokens: 20
- prompt: |
- Is this GitHub pull request spam, AI-generated slop, or low quality?
-
- Title: ${{ env.PR_TITLE }}
- Body: ${{ env.PR_BODY }}
-
- Respond with exactly one of: spam, ai-generated, needs-review, ok
- system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
- model: openai/gpt-4o-mini
-
- - name: Apply quality label if needed
- if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
- uses: actions/github-script@v9
- id: quality-label
- env:
- QUALITY_LABEL: ${{ steps.quality.outputs.response }}
- with:
- script: |
- const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
- const labelMeta = {
- 'spam': { color: 'd73a4a', description: 'Possible spam' },
- 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
- 'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
- };
- const meta = labelMeta[label];
- if (!meta) return;
-
- // Ensure label exists
- try {
- await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
- } catch (e) {
- if (e.status !== 404) throw e;
- await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
- }
-
- // Apply label
- await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
-
- core.setOutput('is_spam', 'true');
-
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Step 3: Auto-label PR type (bugfix/enhancement/refactor)
- # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- - name: Classify PR for labeling
- if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '')
- uses: actions/ai-inference@v2
- id: classify
- continue-on-error: true
- env:
- PR_TITLE: ${{ github.event.pull_request.title }}
- PR_BODY: ${{ github.event.pull_request.body }}
- with:
- max-tokens: 30
- prompt: |
- Classify this pull request for the Meshtastic Android app into exactly one category.
-
- Return exactly one of: bugfix, enhancement, refactor, chore, ci, build, testing, documentation
-
- Label definitions:
- - bugfix: Fixes a bug, crash, or incorrect behavior
- - enhancement: Adds a new feature, improves UX, adds new functionality, or improves performance
- - refactor: Restructures code without changing behavior, cleans up code, or improves architecture
- - chore: Routine maintenance (dependency updates, config tweaks, version bumps) that doesn't change app behavior
- - ci: Changes to CI/CD workflows, GitHub Actions, or automation scripts
- - build: Changes to build system, Gradle config, build-logic, or compilation settings
- - testing: Adds or modifies tests without changing production code
- - documentation: Documentation-only changes (README, CHANGELOG, code comments, KDoc)
-
- Title: ${{ env.PR_TITLE }}
- Body: ${{ env.PR_BODY }}
- system-prompt: >
- You classify pull requests into categories for changelog generation.
- Pick the single most specific label. Prefer ci/build/chore/testing/documentation
- over refactor when the change clearly fits those narrower categories.
- Only use bugfix for actual bug fixes, not for CI fixes or build fixes.
- Only use enhancement for user-facing features, not for internal improvements.
- model: openai/gpt-4o-mini
-
- - name: Apply type label
- if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
- uses: actions/github-script@v9
- env:
- TYPE_LABEL: ${{ steps.classify.outputs.response }}
- with:
- script: |
- const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
- const labelMeta = {
- 'bugfix': { color: 'd73a4a', description: 'Bug fix' },
- 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
- 'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' },
- 'chore': { color: 'ededed', description: 'Routine maintenance' },
- 'ci': { color: 'bfd4f2', description: 'CI/CD and automation changes' },
- 'build': { color: 'bfd4f2', description: 'Build system changes' },
- 'testing': { color: 'f9d0c4', description: 'Test additions or modifications' },
- 'documentation': { color: '0075ca', description: 'Documentation changes' },
- };
- const meta = labelMeta[label];
- if (!meta) return;
-
- // Ensure label exists
- try {
- await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
- } catch (e) {
- if (e.status !== 404) throw e;
- await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
- }
-
- // Apply label
- await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
diff --git a/.github/workflows/moderate.yml b/.github/workflows/moderate.yml
deleted file mode 100644
index b867eeddc5..0000000000
--- a/.github/workflows/moderate.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: AI Moderator
-on:
- workflow_dispatch:
-
-jobs:
- spam-detection:
- if: github.repository == 'meshtastic/Meshtastic-Android'
- runs-on: ubuntu-24.04-arm
- permissions:
- issues: write
- pull-requests: write
- models: read
- contents: read
- steps:
- - uses: actions/checkout@v7.0.0
- - uses: github/ai-moderator@v1
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- spam-label: 'spam'
- ai-label: 'ai-generated'
- minimize-detected-comments: true
- # Built-in prompt configuration (all enabled by default)
- enable-spam-detection: true
- enable-link-spam-detection: true
- enable-ai-detection: true
- # custom-prompt-path: '.github/prompts/my-custom.prompt.yml' # Optional
diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
index 84c75807e8..307ff19ec1 100644
--- a/.github/workflows/update-changelog.yml
+++ b/.github/workflows/update-changelog.yml
@@ -82,7 +82,9 @@ jobs:
CLOSED_TAG: ${{ steps.tags.outputs.closed }}
OPEN_TAG: ${{ steps.tags.outputs.open }}
run: |
- # Helper: generate notes using GitHub API (only works when previous_tag is a release tag)
+ # Helper: generate categorized notes via GitHub API, honoring
+ # .github/release.yml categories + excludes. Works with ANY existing
+ # tag as previous_tag (release OR channel), target = a tag or a SHA.
generate_notes_api() {
local target_sha="$1" previous_tag="$2"
local body
@@ -101,17 +103,6 @@ jobs:
| sed -e :a -e '/^\n*$/{$d;N;ba}'
}
- # Helper: generate notes using GitHub compare API (works between any two refs)
- generate_notes_git() {
- local from_ref="$1" to_ref="$2"
- gh api "repos/${REPO}/compare/${from_ref}...${to_ref}" \
- --jq '.commits[]
- | select(.parents | length == 1)
- | select((.commit.message | split("\n")[0] | ascii_downcase | test("update changelog(\\.md)?")) | not)
- | "* \(.commit.message | split("\n")[0]) by \(.commit.author.name) (@\(.author.login // .commit.author.name)) in [`\(.sha[0:9])`](https://github.com/'"${REPO}"'/commit/\(.sha))"' \
- 2>/dev/null || true
- }
-
# Helper: demote ### headings to #### for nesting under channel sections
demote_headings() {
sed 's/^### /#### /g'
@@ -172,19 +163,13 @@ jobs:
# === Generate segments (oldest to newest, each relative to its predecessor) ===
SECTIONS=""
PREV_REF="$PROD_TAG"
- PREV_IS_PROD=true
for ((i=0; i<N; i++)); do
LABEL="${TAG_LABELS[$i]}"
TAG="${TAG_NAMES[$i]}"
SHA="${TAG_SHAS[$i]}"
- # Use GitHub API when previous ref is a production tag, git log otherwise
- if [ "$PREV_IS_PROD" = true ]; then
- NOTES=$(generate_notes_api "$SHA" "$PREV_REF" | demote_headings)
- else
- NOTES=$(generate_notes_git "$PREV_REF" "$SHA")
- fi
+ NOTES=$(generate_notes_api "$SHA" "$PREV_REF" | demote_headings)
if [ -n "$NOTES" ]; then
SECTION=$(cat <<SECTION
@@ -205,7 +190,6 @@ jobs:
fi
PREV_REF="$TAG"
- PREV_IS_PROD=false
done
# === Unreleased segment (HEAD -> newest tag) ===
@@ -219,12 +203,12 @@ jobs:
LATEST_TAG="$PROD_TAG"
fi
if [ "$HEAD_SHA" != "$(git rev-parse "$LATEST_TAG" 2>/dev/null)" ]; then
- UNRELEASED_COMMITS=$(generate_notes_git "$LATEST_TAG" "$HEAD_SHA")
- if [ -n "$UNRELEASED_COMMITS" ]; then
+ UNRELEASED_NOTES=$(generate_notes_api "$HEAD_SHA" "$LATEST_TAG" | demote_headings)
+ if [ -n "$UNRELEASED_NOTES" ]; then
UNRELEASED_SECTION=$(cat <<SECTION
### Unreleased (not yet in any build)
- ${UNRELEASED_COMMITS}
+ ${UNRELEASED_NOTES}
SECTION
)
fi
Served by rngit 1.5.2 - Generated in 0.66s